package com.mybatis.plus.config;

import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.mybatis.plus.join.JoinWrapper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.apache.ibatis.type.UnknownTypeHandler;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static com.baomidou.mybatisplus.core.toolkit.Constants.*;

@SuppressWarnings("unchecked")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
@Component
public class DynamicResultMapInterceptor implements Interceptor {
    /**
     * 将MappedStatement缓存下来
     */
    private static final Map<String, Map<Configuration, MappedStatement>> CACHE = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        if (!(args[0] instanceof MappedStatement)) {
            return invocation.proceed();
        }
        MappedStatement ms = (MappedStatement) args[0];
        if (!(args[1] instanceof Map)) {
            return invocation.proceed();
        }
        Map<String, ?> map = (Map<String, ?>) args[1];
        if (CollectionUtils.isNotEmpty(map)
                && map.values().stream().anyMatch(v -> v instanceof JoinWrapper)
                && CollectionUtils.isNotEmpty(ms.getResultMaps())
        ) {
            JoinWrapper<?> joinWrapper = (JoinWrapper<?>) map.values()
                    .stream().filter(v -> v instanceof JoinWrapper)
                    .findFirst().orElse(null);
            if (joinWrapper != null && joinWrapper.getResultType() != null) {
                args[0] = newMappedStatement(ms, joinWrapper.getResultType());
            }
        }
        return invocation.proceed();
    }

    /**
     * 构建新的MappedStatement
     */
    public MappedStatement newMappedStatement(MappedStatement ms, Class<?> resultType) {
        String id = ms.getId() + StringPool.UNDERSCORE + resultType.getName();
        Map<Configuration, MappedStatement> statementMap = CACHE.get(id);
        if (CollectionUtils.isNotEmpty(statementMap)) {
            MappedStatement statement = statementMap.get(ms.getConfiguration());
            if (Objects.nonNull(statement)) {
                return statement;
            }
        }
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), id, ms.getSqlSource(), ms.getSqlCommandType())
                .resource(ms.getResource())
                .fetchSize(ms.getFetchSize())
                .statementType(ms.getStatementType())
                .keyGenerator(ms.getKeyGenerator())
                .timeout(ms.getTimeout())
                .parameterMap(ms.getParameterMap())
                .resultSetType(ms.getResultSetType())
                .cache(ms.getCache())
                .flushCacheRequired(ms.isFlushCacheRequired())
                .useCache(ms.isUseCache());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            builder.keyProperty(String.join(StringPool.COMMA, ms.getKeyProperties()));
        }
        List<ResultMap> resultMaps = new ArrayList<>();
        String resource = resultType.getName().replace('.', '/') + ".java (best guess)";
        MapperBuilderAssistant assistant = new MapperBuilderAssistant(ms.getConfiguration(), resource);
        TableInfo tableInfo = TableInfoHelper.initTableInfo(assistant, resultType);
        resultMaps.add(init(tableInfo, ms));
        builder.resultMaps(resultMaps);
        MappedStatement mappedStatement = builder.build();
        if (statementMap == null) {
            statementMap = new ConcurrentHashMap<>();
            CACHE.put(id, statementMap);
        }
        statementMap.put(ms.getConfiguration(), mappedStatement);
        return mappedStatement;
    }

    private ResultMap init(TableInfo tableInfo, MappedStatement ms) {
        String id = ms.getId() + DOT + MYBATIS_PLUS + UNDERSCORE + tableInfo.getEntityType().getSimpleName();
        List<ResultMapping> resultMappings = new ArrayList<>();
        if (tableInfo.getKeyType() != null) {
            ResultMapping idMapping = new ResultMapping.Builder(tableInfo.getConfiguration(), tableInfo.getKeyProperty(), tableInfo.getKeyColumn(), tableInfo.getKeyType())
                    .flags(Collections.singletonList(ResultFlag.ID)).build();
            resultMappings.add(idMapping);
        }
        if (CollectionUtils.isNotEmpty(tableInfo.getFieldList())) {
            tableInfo.getFieldList().forEach(i -> resultMappings.add(get(tableInfo, i)));
        }
        ResultMap resultMap = new ResultMap.Builder(tableInfo.getConfiguration(),
                id, tableInfo.getEntityType(), resultMappings).build();
        tableInfo.getConfiguration().addResultMap(resultMap);
        return resultMap;
    }

    private ResultMapping get(TableInfo tableInfo, TableFieldInfo fieldInfo) {
        ResultMapping.Builder builder = new ResultMapping.Builder(tableInfo.getConfiguration(), fieldInfo.getProperty(),
                StringUtils.getTargetColumn(fieldInfo.getColumn()), fieldInfo.getPropertyType());
        TypeHandlerRegistry registry = tableInfo.getConfiguration().getTypeHandlerRegistry();
        if (fieldInfo.getJdbcType() != null && fieldInfo.getJdbcType() != JdbcType.UNDEFINED) {
            builder.jdbcType(fieldInfo.getJdbcType());
        }
        if (fieldInfo.getTypeHandler() != null && fieldInfo.getTypeHandler() != UnknownTypeHandler.class) {
            TypeHandler<?> typeHandler = registry.getMappingTypeHandler(fieldInfo.getTypeHandler());
            if (typeHandler == null) {
                typeHandler = registry.getInstance(fieldInfo.getPropertyType(), fieldInfo.getTypeHandler());
            }
            builder.typeHandler(typeHandler);
        }
        return builder.build();
    }

}